home *** CD-ROM | disk | FTP | other *** search
/ MacAddict 108 / MacAddict108.iso / Software / Internet & Communication / JunkMatcher 1.5.5.dmg / JunkMatcher.app / Contents / Resources / Engine / SiteDB.py < prev    next >
Encoding:
Python Source  |  2005-06-01  |  20.6 KB  |  619 lines

  1. #
  2. #  SiteDB.py
  3. #  JunkMatcher
  4. #
  5. #  Created by Benjamin Han on 2/1/05.
  6. #  Copyright (c) 2005 Benjamin Han. All rights reserved.
  7. #
  8.  
  9. # This program is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU General Public License
  11. # as published by the Free Software Foundation; either version 2
  12. # of the License, or (at your option) any later version.
  13.  
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17. # GNU General Public License for more details.
  18.  
  19. # You should have received a copy of the GNU General Public License
  20. # along with this program; if not, write to the Free Software
  21. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  22.  
  23. #!/usr/bin/env python
  24.  
  25. # IMPORTANT: SiteDB can have only a single instance!
  26.  
  27. import datetime, copy, string
  28.  
  29. from consts import *
  30. from utilities import *
  31. from rwlock import *     # to ensure thread-safety, since we will have only one global SiteDB
  32.  
  33.  
  34. _gTLD=sets.Set(openFile('%setc/gTLD' % ENGINE_PATH).read().split('\n')[:-1])
  35. _gTLD.add('arpa')   # to be 100% precise arpa is not a generic TLD
  36. _ccTLD=sets.Set(openFile('%setc/ccTLD' % ENGINE_PATH).read().split('\n')[:-1])
  37.  
  38. # SiteDB is implemented to accomodate multiple readers and (xor) single writer
  39. _siteDBRWLock = RWLock()
  40.  
  41.  
  42. class SiteDB (dict):
  43.     """Bad sites collection: a tree
  44.        ----------------------------
  45.        fn: the filename
  46.        count: the # of unique sites (or leaf nodes)
  47.        sizeLimit: the max capacity (count will be kept less than sizeLimit; 0 with no limit)
  48.  
  49.        Note: each node is a list [time, count, level, children]
  50.          - time: # of seconds since epoch in UTC when the last time the node is updated
  51.          - count: # of occurrences
  52.          - level: depth in the tree (0 is the top-level, i.e., TLD)
  53.          - children: a dictionary or None (if it's a leaf)
  54.        """
  55.     def __init__ (self, fn, sizeLimit = 0):
  56.         self.fn = fn
  57.         self.sizeLimit = sizeLimit
  58.  
  59.         self.load()
  60.  
  61.     def __str__ (self):
  62.         """Returns a string representation of the SiteDB; can be used with loadFromString()
  63.         later."""
  64.         _siteDBRWLock.acquire_read()
  65.         ret = '\n'.join(['%s\n%u %d' % ('.'.join(siteInfo[0]), siteInfo[1], siteInfo[2])
  66.                          for siteInfo in self._iter()])
  67.         _siteDBRWLock.release()
  68.  
  69.         return ret
  70.  
  71.     def _load (self, s):
  72.         """(INTERNAL) Load from a string 's' - NOT THREAD-SAFE!"""
  73.         self.clear()
  74.         self._count = 0
  75.         
  76.         isName = True
  77.         for l in filter(lambda l:len(l), map(string.strip, s.split('\n'))):
  78.             if isName:
  79.                 site = l.split('.')
  80.                 isName = False
  81.             else:
  82.                 l = l.split(' ')                    
  83.                 self._addOne(site, int(l[0]), int(l[1]), False)
  84.                 isName = True
  85.  
  86.         if self.sizeLimit and self._count >= self.sizeLimit:
  87.             self._prune()
  88.  
  89.     def load (self):
  90.         # NOTE: MULTIPLE THREADS MAY LOAD THE DB MULTIPLE TIMES!
  91.         _siteDBRWLock.acquire_write()
  92.  
  93.         try: f = openFile(self.fn)
  94.         except:
  95.             return
  96.  
  97.         s = f.read()
  98.         self._load(s)
  99.         
  100.         _siteDBRWLock.release()
  101.  
  102.     def loadFromString (self, s):
  103.         # NOTE: MULTIPLE THREADS MAY LOAD THE DB MULTIPLE TIMES!
  104.         _siteDBRWLock.acquire_write()
  105.         
  106.         try:
  107.             self._load(s)
  108.         except Exception, e:
  109.             _siteDBRWLock.release()
  110.             raise e
  111.         
  112.         _siteDBRWLock.release()
  113.  
  114.     def size (self):
  115.         _siteDBRWLock.acquire_read()
  116.         ret = self._count
  117.         _siteDBRWLock.release()
  118.         return ret
  119.  
  120.     def setSizeLimit (self, sizeLimit):
  121.         """Set size limit of the SiteDB; returns True iff pruning occurs (and something did
  122.         get removed during the pruning)."""
  123.         _siteDBRWLock.acquire_write()
  124.         ret = False
  125.  
  126.         try:
  127.             if self.sizeLimit > sizeLimit:
  128.                 self.sizeLimit = sizeLimit
  129.                 oldSize = self._count
  130.                 self._prune()
  131.                 ret = (self._count != oldSize)
  132.             
  133.             else: self.sizeLimit = sizeLimit
  134.  
  135.         except Exception, e:
  136.             printException(u'Exception in SiteDB.setSizeLimit()', e)
  137.             
  138.         _siteDBRWLock.release()
  139.  
  140.         return ret
  141.  
  142.     def _addOne (self, site, timeSec, numTimes, prune):
  143.         """Add one site (a sequence of name components) to the tree - the site can be numerical 
  144.         or non-numerical, and only significant parts are kept: for non-numerical names with 
  145.         _gTLD, it's the last 2 components; for non-numerical names with _ccTLD, it's the last 
  146.         3 components; for numerical names it's the 4 components.
  147.  
  148.         Pruning happens when necessary, if prune is set to True.
  149.  
  150.         ASSUMPTION: site has at least two components.
  151.         """
  152.  
  153.         tld = site[-1]
  154.         level = 0
  155.         theDict = self
  156.  
  157.         isGTLD = tld in _gTLD
  158.         isCCTLD = tld in _ccTLD
  159.  
  160.         if not isGTLD and not isCCTLD:
  161.             try:
  162.                 int(tld)
  163.             except:
  164.                 # bogus TLD
  165.                 return
  166.             
  167.             # numerical names - add from the first component forward
  168.             for comp in site:
  169.                 if theDict is None:
  170.                     theDict = {}
  171.                     node[-1] = theDict
  172.  
  173.                 if level < 3:
  174.                     node = theDict.setdefault(comp, [0, 0, level, {}])
  175.                     theDict = node[-1]
  176.                     level += 1
  177.                 else:
  178.                     node = theDict.setdefault(comp, [0, 0, level, None])
  179.                     break
  180.  
  181.             if level != 3:
  182.                 # bogus IP address (must have 4 components)
  183.                 return
  184.             
  185.         else:
  186.             if isGTLD: count = 1
  187.             else: count = 2
  188.  
  189.             # non-numerical names - add from the last component backward
  190.             for comp in site[::-1]:
  191.                 if theDict is None:
  192.                     theDict = {}
  193.                     node[-1] = theDict
  194.                 if level == count:
  195.                     node = theDict.setdefault(comp, [0, 0, level, None])
  196.                     break
  197.                 else:
  198.                     node = theDict.setdefault(comp, [0, 0, level, {}])
  199.                     theDict = node[-1]
  200.                 level += 1
  201.  
  202.         node[1] += numTimes
  203.         nodeTime = node[0]
  204.         
  205.         if nodeTime == 0:            
  206.             self._count += 1       # this is the first time the site is added
  207.             node[0] = timeSec
  208.             if prune and self.sizeLimit and self._count > self.sizeLimit:
  209.                 self._prune()
  210.                 
  211.         elif nodeTime < timeSec:
  212.             node[0] = timeSec      # update to the latest time
  213.  
  214.     def addOne (self, site, timeSec, numTimes = 1, prune = True):
  215.         """A thread-safe version of _addOne()."""
  216.         _siteDBRWLock.acquire_write()
  217.         ret = self._addOne(site, timeSec, numTimes, prune)
  218.         _siteDBRWLock.release()
  219.         
  220.         return ret
  221.         
  222.     def getOne (self, site):
  223.         """Try to add a site (a sequence of name components) into the collection; returns a 
  224.         tuple (l, n, b), where l is a list of not-yet-added but significant name components, 
  225.         n is the deepest node we can find, and b is a boolean:
  226.  
  227.         1. l is None if the last component is bogus (not one of the valid TLDs)
  228.         2. n is None if the last component is not in the collection (in that case l is
  229.            the full list of name components)
  230.         3. b is True iff site is *not* numerical
  231.  
  232.         Examples:
  233.         1. sites.getOne(('yahoo','com')) -> ((), n, True), where n is not None: 'yahoo.com' is
  234.            in the collection;
  235.         2. sites.getOne(('www','yahoo','com')) -> ((), n, True), where n is not None: same as
  236.            above;
  237.         3. sites.getOne(('cnet','com')) -> (('cnet'), n, True), where n is not None: there is
  238.            already at least one .com site added, but cnet.com is not added before;
  239.         4. sites.getOne(('cnet','com')) -> (('cnet','com'), None, True): no .com site is added
  240.            before;
  241.         5. sites.getOne(('rubbish','bogus')) -> (None, None, True); the last component is bogus.
  242.         6. sites.getOne(('128','2','3','4')) -> (('2','3','4'), None, False); at least one IP
  243.            starting with 128 has been added before.
  244.         """
  245.  
  246.         tld = site[-1]
  247.         theDict = self
  248.         prevNode = None
  249.  
  250.         if tld in _gTLD:
  251.             count = 2
  252.         elif tld in _ccTLD:
  253.             count = 3
  254.         else:
  255.             try:
  256.                 int(tld)
  257.                 count = 0
  258.             except:
  259.                 return None, None, True
  260.  
  261.         _siteDBRWLock.acquire_read()
  262.         
  263.         if count > 0:
  264.             # non-numerical name
  265.             for comp in site[::-1]:
  266.                 node = theDict.get(comp)
  267.                 if node is None: break
  268.                 else:
  269.                     count -= 1                    
  270.                     prevNode = node
  271.                     theDict = node[-1]
  272.                     if theDict is None: break
  273.  
  274.             if prevNode is None:
  275.                 _siteDBRWLock.release()
  276.                 return site[-count:], None, True
  277.             else:
  278.                 endPos = -(prevNode[2] + 1)
  279.                 _siteDBRWLock.release()
  280.                 return site[endPos-count:endPos], prevNode, True
  281.  
  282.         else:
  283.             # numerical name
  284.             for comp in site:
  285.                 try: int(comp)
  286.                 except:
  287.                     _siteDBRWLock.release()
  288.                     return None, None, True
  289.                 
  290.                 node = theDict.get(comp)
  291.                 if node is None: break
  292.                 else:
  293.                     count += 1
  294.                     prevNode = node
  295.                     theDict = node[-1]
  296.                     if theDict is None: break
  297.  
  298.             _siteDBRWLock.release()
  299.             return site[count:4], prevNode, False
  300.  
  301.     def addOneToNode (self, nameComps, node, notNumerical, timeSec, numTimes = 1):
  302.         """Add one site with prefix/suffix in nameComps (a sequence; prefix if notNumerical is
  303.         True, suffix otherwise) to node, so that in effect the complete name of the site is
  304.         '.'.join(nameComps)+suffix (non-numeric), or prefix+'.'.join(nameComps) (numeric),
  305.         where suffix/prefix is the name components collected from the root node to 'node'.
  306.  
  307.         Pruning happens when necessary.
  308.  
  309.         ASSUMPTION: nameComps contains only the significant components (based on what
  310.         the TLD is) - this function is intended to be used with getOne().
  311.  
  312.         TO-DO: if any exception occurs _siteDBRWLock might still be locked in write mode!
  313.         """
  314.         if len(nameComps):
  315.             level = node[2] + 1
  316.             theDict = node[-1]
  317.  
  318.             if notNumerical:
  319.                 nameComps.reverse()
  320.  
  321.             _siteDBRWLock.acquire_write()
  322.             
  323.             for comp in nameComps[:-1]:
  324.                 if theDict is None:
  325.                     theDict = {}
  326.                     node[-1] = theDict
  327.                 node = theDict.setdefault(comp, [0, 0, level, {}])
  328.                 theDict = node[-1]
  329.                 level += 1
  330.             if theDict is None:
  331.                 theDict = {}
  332.                 node[-1] = theDict
  333.             node = theDict.setdefault(nameComps[-1], [0, 0, level, None])
  334.  
  335.         else: _siteDBRWLock.acquire_write()
  336.  
  337.         node[1] += numTimes
  338.         nodeTime = node[0]
  339.         if nodeTime == 0:
  340.             self._count += 1
  341.             node[0] = timeSec
  342.             if self.sizeLimit and self._count > self.sizeLimit:
  343.                 self._prune()
  344.         elif nodeTime < timeSec:
  345.             node[0] = timeSec
  346.  
  347.         _siteDBRWLock.release()
  348.  
  349.     def _removeOne (self, site, countMatters = False):
  350.         """Removes a site (a sequence of name components) from the collection: if countMatters 
  351.         is True, decrements the count by 1 (and removes the entire entry if count becomes 0); 
  352.         if not, removes the entire entry; in both cases the timestamp will not be updated.
  353.  
  354.         Returns True iff change did occur.
  355.  
  356.         ASSUMPTION: site is syntactically correct (no int(.) test on each component of an
  357.         IP address).
  358.  
  359.         WARNING: NOT thread-safe - use removeOne() for that!
  360.         """
  361.  
  362.         tld = site[-1]
  363.         theDict = self
  364.         path = []
  365.  
  366.         if tld in _gTLD:
  367.             count = 2
  368.         elif tld in _ccTLD:
  369.             count = 3
  370.         else:
  371.             try:
  372.                 int(tld)
  373.                 count = 0
  374.             except: return False        
  375.  
  376.         # find a path from the leaf to the root
  377.         if count > 0:
  378.             for comp in site[::-1]:
  379.                 node = theDict.get(comp)
  380.                 if node is None:
  381.                     return False
  382.                 else:
  383.                     path.append([comp, node])
  384.                     count -= 1
  385.                     if count == 0: break
  386.                     theDict = node[-1]
  387.                     if theDict is None:
  388.                         return False
  389.  
  390.         else:            
  391.             for comp in site:
  392.                 node = theDict.get(comp)
  393.                 if node is None:
  394.                     return False
  395.                 else:
  396.                     path.append([comp, node])
  397.                     count += 1
  398.                     if count == 4: break
  399.                     theDict = node[-1]
  400.                     if theDict is None:
  401.                         return False
  402.         
  403.         # remove the path, from bottom up
  404.         if countMatters:
  405.             key, node = path[-1]
  406.  
  407.             # decrement the count
  408.             if node[1]:
  409.                 node[1] -= 1
  410.             else:
  411.                 # not a leaf node - don't do anything
  412.                 return False
  413.  
  414.             if node[1] == 0:
  415.                 # count is decremented to zero for this node, remove it
  416.                 self._count -= 1
  417.                 for nextKey, node in path[-2::-1]:
  418.                     theDict = node[-1]
  419.                     del theDict[key]
  420.  
  421.                     # if this node has other children, or itself represents a site
  422.                     # don't try to remove it from its parent's dictionary
  423.                     if len(theDict) or node[1]: break
  424.                 
  425.                     key = nextKey
  426.                 else:
  427.                     theDict = self
  428.                     del theDict[key]
  429.  
  430.         else:
  431.             self._count -= 1
  432.             key = path[-1][0]
  433.             for nextKey, node in path[-2::-1]:
  434.                 theDict = node[-1]
  435.                 del theDict[key]
  436.  
  437.                 # if this node has other children, or itself represents a site
  438.                 # don't try to remove it from its parent's dictionary
  439.                 if len(theDict) or node[1]: break
  440.                 
  441.                 key = nextKey
  442.             else:
  443.                 theDict = self
  444.                 del theDict[key]
  445.  
  446.         return True
  447.  
  448.     def removeOne (self, site, countMatters = False):
  449.         """A thread-safe version of _removeOne()."""
  450.         _siteDBRWLock.acquire_write()
  451.  
  452.         try:
  453.             ret = self._removeOne(site, countMatters)
  454.         except Exception, e:
  455.             printException(u'Exception in SiteDB.removeOne().', e)
  456.             
  457.         _siteDBRWLock.release()
  458.  
  459.         return ret
  460.  
  461.     def _prune (self):
  462.         """Recency-based pruning: throw away the oldest n records; NOT thread-safe (use prune()
  463.         for that)."""
  464.         # n is the # of entries we want to prune
  465.         n = self._count - int(self.sizeLimit * SAFE_SITE_SIZE_RATIO)
  466.         if n <= 0: return
  467.  
  468.         l = [[[siteInfo[1], siteInfo[2]], copy.copy(siteInfo[0])]
  469.              for siteInfo in self._iter()]
  470.         l.sort()   # sorted by time, then # of occurrence, then names
  471.         for i in l[:n]: self._removeOne(i[1])
  472.  
  473.     def prune (self):
  474.         """A thread-safe version of _prune()."""
  475.         _siteDBRWLock.acquire_write()
  476.         
  477.         try:
  478.             self._prune()
  479.         except Exception, e:
  480.             printException(u'Exception in SiteDB.prune().', e)
  481.  
  482.         _siteDBRWLock.release()        
  483.  
  484.     def recycle (self):
  485.         _siteDBRWLock.acquire_write()
  486.  
  487.         try:
  488.             self.clear()
  489.             self._count = 0
  490.             os.remove(self.fn)
  491.             
  492.         except Exception, e:
  493.             printException(u'Exception in SiteDB.recycle()', e)
  494.             
  495.         _siteDBRWLock.release()
  496.  
  497.     def _iter (self):
  498.         """Iterates over the tree. Returns a list [site name, time, # of occurrences], where
  499.         site name is a list of name components, time is # of seconds since epoch when the last
  500.         time the site is updated.
  501.  
  502.         WARNING: NOT THREAD-SAFE!
  503.         """
  504.         stack = [[k,v] for k, v in self.items()]
  505.         nameComps = []
  506.         while len(stack):
  507.             node = stack[0]
  508.             del stack[0]
  509.  
  510.             payload = node[-1]
  511.             level = payload[2]
  512.  
  513.             if level == 0: del nameComps[:]
  514.             elif level < len(nameComps): del nameComps[0:-level]
  515.             nameComps.insert(0, node[0])
  516.  
  517.             if payload[1]:
  518.                 # it's a site
  519.                 try:
  520.                     int(nameComps[-1])
  521.                     yield [nameComps[::-1], payload[0], payload[1]]
  522.                 except:
  523.                     # non-numerical name
  524.                     yield [nameComps,payload[0], payload[1]]
  525.                         
  526.             if payload[-1] is not None:
  527.                 # not a leaf node - push the children
  528.                 for k,v in payload[-1].items()[::-1]:
  529.                     stack.insert(0,[k,v])
  530.  
  531.     def removeSitesUsingPattern (self, sitesPattern):
  532.         """Removes any site that matches sitesPattern (a regex pattern); returns True iff
  533.         there's any change."""
  534.         removeList = []
  535.         
  536.         _siteDBRWLock.acquire_read()
  537.         for siteInfo in self._iter():
  538.             siteName = '.'.join(siteInfo[0])
  539.             if sitesPattern.search(siteName): removeList.append(siteName)
  540.         _siteDBRWLock.release()
  541.  
  542.         # it's possible some sites are already gone by now (since it's not locked here)
  543.         # but it's okay - removeOne() is tolerant enough
  544.         if len(removeList):
  545.             _siteDBRWLock.acquire_write()
  546.  
  547.             try:
  548.                 noChange = True
  549.                 for site in removeList:
  550.                     if self._removeOne(site.split('.')) and noChange:
  551.                         noChange = False
  552.             except Exception, e:
  553.                 printException(u'Exception in SiteDB.removeSitesUsingPattern()', e)
  554.  
  555.             _siteDBRWLock.release()
  556.             return not noChange
  557.  
  558.         return False
  559.  
  560.     def writeToFile (self, fn = None):
  561.         if fn: f = openFile(fn, 'w')
  562.         else: f = openFile(self.fn, 'w')
  563.         f.write(self.__str__())
  564.  
  565.     def loadIntoList (self, siteList):
  566.         """Load into siteList - a list of dict with keys 'name', 'time', and 'count'."""
  567.         _siteDBRWLock.acquire_read()
  568.         
  569.         tmpList = map(lambda siteInfo: (siteInfo[1],
  570.                                         {'name':'.'.join(siteInfo[0]),
  571.                                          'time':datetime.datetime.utcfromtimestamp(siteInfo[1]),
  572.                                          'count':siteInfo[2]}),
  573.                       self._iter())
  574.         
  575.         tmpList.sort()
  576.         tmpList.reverse()
  577.         siteList[:] = map(lambda i:i[1], tmpList)
  578.             
  579.         _siteDBRWLock.release()
  580.         
  581.  
  582. if __name__ == '__main__':
  583.     siteDB = SiteDB('%ssiteDB' % CONF_PATH, 2000)
  584.  
  585.     l, n, b = siteDB.getOne(('xyz', 'com'))
  586.     print l, n is not None, b
  587.     l, n, b = siteDB.getOne(('xyz', 'abc', 'biz'))
  588.     print l, n is not None, b
  589.     l, n, b = siteDB.getOne(('xyz', 'abc', 'com', 'tw'))
  590.     print l, n is not None, b
  591.     l, n, b = siteDB.getOne(('xyz', 'abc', 'com', 'il'))
  592.     print l, n is not None, b
  593.     l, n, b = siteDB.getOne(('com', ))   # so don't pass this to SiteDB!
  594.     print l, n is not None, b
  595.     l, n, b = siteDB.getOne(('rubbish', 'bogus'))
  596.     print l, n is not None, b
  597.     l, n, b = siteDB.getOne(('66', '0', '78', '20'))
  598.     print l, n is not None, b
  599.  
  600.     print
  601.     print '* before removal:', siteDB.size()
  602.     print '* any "biz" site removed?', siteDB.removeSitesUsingPattern(re.compile(r'\.biz$'))
  603.     print '* any "edu" site removed?', siteDB.removeSitesUsingPattern(re.compile(r'\.edu$'))
  604.     print '* after removal:', siteDB.size()
  605.     print
  606.  
  607.     siteDB.removeOne('24info.com.tw'.split('.'))
  608.     s = str(siteDB)
  609.     siteDB.loadFromString(s)
  610.  
  611.     # test writing the content to a file
  612.     #siteDB.writeToFile('sites.out')
  613.     #print '* Wrote %d sites to "sites.out".' % siteDB.size()
  614.  
  615.     #siteList = []
  616.     #siteDB.loadIntoList(siteList)
  617.     #print '\n'.join(['%s (%d): %s' % (s['name'], s['count'], s['time'])
  618.     #                 for s in siteList])
  619.